Claude Code ソースコード解説シリーズ 第14章: Plan
Claude Code が Plan Mode を権限境界つきのランタイム機構に変える方法を説明します。
『Claude Code ソースコード解析シリーズ』第14章|Plan
Claude Code の Plan Mode を初めて触った多くの人は、素朴な理解にとどまっている。
モデルに計画を書かせて、ユーザーが承認したらコード変更を始める仕組み。
この理解は間違ってはいないが、浅すぎる。
単にプロンプトに「まず計画を立ててから実行してください」と書いただけなら、それは行動の提案にすぎない。モデルの気分次第で守ったりスキップしたりするだけで、厳格な境界はない。Claude Code が本当に面白いのは、「考えてから動く」をランタイムメカニズムに落とし込んでいる点だ。
ユーザーの目標
-> Plan Mode に入る
-> 読み取り専用権限に切り替え
-> コードベースのコンテキストを収集
-> 承認可能な計画を生成
-> ユーザーが承認
-> 元の権限モードに復帰
-> 実行フェーズに移行
-> 計画に沿って実装・検証
Plan は丁寧に書かれたプロンプトではない。ツールセット、権限モード、状態保存、サブエージェント、承認 UI、計画ファイル、復帰メカニズムによって組み立てられた一層の制御面だ。
この記事ではそれを分解していく。
まずソースコードレベルのキーワードを押さえておこう。Claude Code において、plan は PermissionMode の明示的な列挙値であり、default、acceptEdits、bypassPermissions、auto と同列に位置する。独自のタイトル、短縮タイトル、アイコン、カラー、external マッピングを持つ。つまり Plan は、あるツールが一時的に付与した状態フラグなどではなく、ツールの権限判定チェーン全体を通るランタイムモードなのだ。
1. Planが解決するのは「プランが書けないこと」ではなく「性急な行動」である
エージェントがコードを書くとき、最も危険な瞬間は往々にして、1行のコードを間違えることではない。プロジェクトをまだ理解していないのに、すでに手を動かし始めていることだ。
たとえば「インターフェースにページングを追加して」と依頼する。エージェントはすぐさま page や pageSize を追加し、ついでにいくつかのハンドラを修正するかもしれない。そして修正を終えた後にようやく気づくのだ。
- プロジェクトにはすでに統一されたレスポンス形式が存在する
- データ層にはPrismaが使われている
- 既存のインターフェースは
limit/offset寄りである - バリデーション層はZodで統一されている
- 一部のリストインターフェースはカーソルベースのページングにより適している
- 変更にはフロントエンドの既存呼び出しとの互換性も必要である
エージェントがすでに大量のコードを書いてしまっていれば、これらの発見は手戻りになる。読み取り専用の段階にとどまっていれば、これらの発見は単なる計画材料にすぎず、コストはゼロだ。
したがって、Plan Modeの核心的な問いは次のようなものではない。
モデルにどうやって立派な計画を生成させるか?
そうではなく、こうだ。
エージェントに「プロジェクトを十分に理解できるが、副作用を生まない」正当な段階を、どう与えるか?
Claude Codeの答えは、作業を2つの段階に分割することである。
Plan段階:コードを読む、コンテキストを検索する、方式を比較する、質問する、計画を出力する
Execute段階:ファイルを書く、コマンドを実行する、コードを修正する、結果を検証する
率直に言えば、Plan Modeの第一層の本質は 読み取りと書き込みの分離 である。
2. 入口:EnterPlanModeTool——「計画」を状態切り替えとして扱う
Claude Code において、Plan Mode への移行は単なるモデルの口調変更ではない。EnterPlanModeTool によって、ランタイムの状態切り替えが一度だけ発火する。
ローカルのソースコードから読み取れるこの入口の大まかな流れは、次のようになる。
ユーザーまたはモデルが Plan Mode への移行を決定
-> 現在サブ Agent コンテキスト内にいるかを確認
-> Plan に入る前の権限モードを prePlanMode に保存
-> Plan Mode 用の権限コンテキストを準備
-> 現在の permission mode を plan に切り替え
-> Plan Mode での振る舞い指示を返す
よりソースコードに即して言うならば、EnterPlanModeTool は自身を shouldDefer: true、isConcurrencySafe: true、isReadOnly: true として宣言している。実際の呼び出し時には、現在の AppState を読み取り、prepareContextForPlanMode(prev.toolPermissionContext) で権限コンテキストを準備し、applyPermissionUpdate(..., { type: "setMode", mode: "plan", destination: "session" }) によってセッションを plan に切り替える。
モデルに返される tool result には「コードベースを探索し、実装方針を設計せよ。ファイルの書き込みや編集は行うな」といった内容が強調されている。しかし、そのテキストはあくまで外側の振る舞い説明に過ぎない。実際のハードな切り替えは toolPermissionContext.mode = "plan" の一行で起こる。
ここには二つの重要なポイントがある。
第一に、prePlanMode。
Plan Mode は永続的なモードではなく、実行前の一時的な状態に過ぎない。Plan に入る前、システムは default、auto、acceptEdits など、さまざまな権限モードで動作している可能性がある。Plan を抜けたあとに元の実行コンテキストへ戻れるよう、Claude Code は元のモードをあらかじめ保存しておく。
(この設計自体はごく一般的だが、見落とされがちでもある。保存を忘れると、Plan 終了後の権限が噛み合わなくなり、Agent が「毎回確認を求める」状態からいきなり「すべて自動承認」になったり、その逆が起こったりする。)
第二に、子 Agent のコンテキスト制限
ローカルの資料によれば、子 Agent は自ら Plan Mode に入るべきではないとされている。理由は単純だ。Plan Mode は最終的にユーザーの承認を必要とする。ユーザーから見えない子 Agent が途中で承認リクエストを出しても、親 Agent はブロックされ、ユーザーも誰の計画を承認すべきか判断できない。Plan はメインフローの制御面であり、サブタスクの奥深くに潜ませるものではない。
このことは、Claude Code が Plan を明確に位置づけていることを示している。すなわち、Plan とは任意の Agent が気軽に起動できる「思考スイッチ」などではなく、実行境界を制御するためにメインセッションで用いられるフェーズなのだ。
3. Plan Mode のハードバウンダリー:権限システムが書き込み操作をブロックする
Plan Mode で最も過小評価されがちなのが、ツール権限が実際に変わるという点だ。
権限モデルにおいて、plan は独立したモードである。その意味するところは「書き込まないほうがいい」という提案ではなく、「読み取り専用でないツールは権限チェックを通過すべきでない」という強制だ。
権限チェックは次のように考えればわかりやすい:
ツール呼び出しが権限パイプラインに入る
-> 現在 plan mode か?
-> ツールが読み取り専用でなければ、即座に拒否
-> Read / Grep / Glob など読み取り専用ツールなら、通常のチェックを続行
「まず手を動かすな」がプロンプトによる制約からランタイムによる制約へと引き上げられている。モデルが変更したくてもできないのだ。
したがって Plan フェーズで Agent が行えるのは次の操作である:
Read:主要ファイルの読み取り
Grep:既存パターンの検索
Glob:プロジェクト構造の発見
LS:ディレクトリ構成の把握
制限付き Bash:許可されたバージョンとポリシーの下での探索的観測
AskUserQuestion:方針選択の確認
ExitPlanMode:計画を提出し承認を待つ
一方、行うべきでないのは次の操作だ:
Edit / Write:ファイルの変更
NotebookEdit:ノートブックの変更
副作用を伴う Bash / ネットワーク / 外部書き込み
承認の混乱を招くサブプロセスのさらなる派生
これが Plan Mode と、単に「モデルにまず考えさせてみる」というアプローチの根本的な違いである。通常のプロンプトはモデルの自制に頼るしかない。Plan Mode はツール面でも範囲を狭める。
ここで一つ境界的事項を補足しておく:Claude Code のドキュメントやバージョンによって、Plan Mode 下での Bash の扱いに関する記述は完全には一致していない。あるものは「探索のためにシェルコマンドを実行できる」と強調し、別のものは「コマンドを実行しない」と概括している。より確実な理解はこうだ。Plan Mode の核心は「Bash というツール名が必ず現れる/必ず消える」ことではなく、変更を伴う副作用を生じさせないことにある。特定の種類のコマンドを呼び出せるかどうかは、現在のバージョン、権限ポリシー、ツールフィルタリングの結果に依存する。
(本番環境では、Bash が必ず無効化されていると決めつけてはいけない。実際のツールフィルタリングリストを確認するほうが確実だ。)
新しいソースコードにおける Plan agent のプロンプトは、この誤解をうまく修正してくれる。ls、git status、git log、git diff、find、cat、head、tail といった読み取り専用の探索コマンドは許可しつつ、mkdir、touch、rm、cp、mv、git add、git commit、npm install、pip install といったワークスペースや環境を変更するコマンドは明示的に禁止しているのだ。
したがって、Plan モードのより正確な境界は次のとおりです。
探索は続行できる。
変更は承認を待たなければならない。
副作用の制約は、ツール自身の checkPermissions、パーミッションチェーン、Plan エージェントのプロンプト、そして終了時の承認によって共同で行われるものであり、「Bash を無効化する」といった単一のルールに依存しているわけではありません。
4. Planサブエージェント:調査コンテキストを隔離する
Claude Codeの公開ドキュメントには、さらに重要な設計レイヤーがもう一つ説明されています。Plan Mode中にClaudeがコードベースを理解する必要がある場合、調査作業を組み込みのPlanサブエージェントに委譲できるというものです。
このPlanサブエージェントの特徴は次のとおりです。
- メインセッションのモデルを継承
- 独立したコンテキストウィンドウを使用
- 読み取り専用ツールのみ利用可能
- 主な役割は計画立案前のコードベース情報収集
- 無制限に子エージェントを生成することは不可
これにより、もう一つのAgentシステムのよくある問題であるコンテキスト汚染が解決されます。
メインスレッドが検索、読み取り、失敗パス、無関係なファイル内容のすべてを同一コンテキストに詰め込んでしまうと、計画ができる前にメイン会話はすでに長大で混沌としたものになっています。Planサブエージェントはいわばリサーチャーです。コードベースに潜って情報を調べ、最終的に結論をメインスレッドに返します。メインスレッドはよりクリーンな視野を保ったまま、ユーザーと最終的な方針をすり合わせることができます。
次のように捉えるとわかりやすいでしょう。
メインAgent:目的理解、ユーザーコミュニケーション、最終計画の合成を担当
Planサブエージェント:読み取り専用の調査、エビデンス収集、パターン発見を担当
権限システム:調査段階で副作用が生じないことを保証
この構造は非常に抑制的です。Claude CodeはPlanを神秘的な中央プランナーに仕立てておらず、固定のplan AST(抽象構文木。計画を構造化して表現する形式)を公開してもいません。むしろ次のような考え方に近いでしょう。
メインスレッドが意思決定を担い、Planサブエージェントが調査を担い、権限システムがガードレールを担う。
ソースコード上では二つの事柄を区別する必要があります。メインセッションのPlan Modeは権限状態の遷移であり、組み込みのPlanエージェントはagent definitionです。後者の定義では、Agent、ExitPlanMode、Edit、Write、NotebookEditといったツールが無効化されており、読み取り専用のリサーチャーとして振る舞い、さらに委譲や実装を続けられる別の実行者にはならないようになっています。
5. ExitPlanModeTool:Plan 終了は Plan 開始より複雑
Plan Mode への移行は主に読み取り専用ステージへの切り替えである。Plan Mode からの終了には、より多くの処理が伴う。
ExitPlanModeV2Tool には少なくとも四つの責務がある:
1. Agent が生成した計画内容を受け取る
2. 計画をユーザーまたはチームリードに提示し、承認を得る
3. Plan 移行前に保存した権限モードを復元する
4. 後続の実行・復旧・検証のために計画素材を保存する
最も注目すべきは権限の復元である。
Plan に移行する前、システムが auto モード(自動モード。Agent が一部の操作を人間の逐一確認なしで実行できる)だったとする。通常、Plan 終了後は auto に復元されるべきであり、そうすればユーザーが計画を承認した後、Agent は従来の自動化戦略のまま実行を継続できる。
しかし、ここにはタイムウィンドウが存在する:
Plan 移行時点では auto が利用可能
-> Agent が Plan Mode 内で十数分間検討を行う
-> その間に、auto gate(自動モードの安全ゲート。特定条件下で自動権限を遮断する保護機構)がセキュリティポリシーまたはサーキットブレーカーによって閉じられる
-> Agent が Plan を終了する
auto が依然として利用可能かどうかを再チェックしなければ、本来使うべきでない高権限モードに Agent を復元してしまう恐れがある。
そのため、ExitPlanMode の復元ロジックは防御的に動作する:元のモードが auto であっても、現在 auto gate が閉じている場合は、より保守的な default モードにフォールバックする。
この細部が示すのは、Claude Code の Plan が「ユーザーが続行ボタンを押すだけ」の薄い UI ではないということだ。Plan は権限状態と密結合したランタイム状態マシンなのである。
もう一つ見落としがちな事実がある:ExitPlanModeV2Tool 自体は read-only ではない。なぜなら、plan をディスクに書き込むからだ。この書き込みは Plan ワークフローが許可する制御された書き込みであり、Plan 段階でプロジェクトのビジネスファイルを変更してよいことを意味しない。終了時には、入力中の plan またはディスク上の plan を読み取り、ユーザーが承認 UI 上で計画を編集していた場合は、編集後の内容をファイルに書き戻す。その上で prePlanMode を復元し、この一時フィールドをクリーンアップする。
通常のメインセッションでは、Plan の終了にはユーザーの対話的な確認が必要です。一方、team のシナリオでは、現在のコンテキストが teammate の場合、終了リクエストはローカル UI を表示せず、team lead のメールボックスに plan_approval_request として書き込まれ、lead の承認を待ってから実装を続行します。
6. 計画は一時的なテキストではなく、復元可能な実行の証憑である
Plan Mode には、もうひとつ工学的に洗練された設計がある。計画はチャットの吹き出しの中だけに存在してはならない、というものだ。
ユーザーはすでに時間をかけて計画を承認し、Agent もトークンを使って調査を行っている。このあとセッションがクラッシュしたり、リモートセッションを復元したり、コンテキストが圧縮されたりしたときに計画が失われれば、実行フェーズは依拠するものを失ってしまう。
そこで Claude Code は、計画を復元可能な成果物として扱う。
デフォルトでは、計画ファイルのディレクトリは ~/.claude/plans である。settings に plansDirectory が設定されている場合、ソースコードはそれをプロジェクトルートからの相対パスとして解決し、プロジェクトルートの外に逃げられないことをチェックする。計画ファイルのパスはセッションの slug によって決まる。メインセッションの場合は {planSlug}.md、サブエージェントのシナリオでは -agent-{agentId} というサフィックスが付与され、異なる実行コンテキスト同士で上書きされるのを防ぐ。
ローカル資料によれば、計画の復元にはおおむね三層のソースがあるという。
第一層:計画ファイルを直接読み取る
第二層:transcript のファイルスナップショットから復元する
第三層:メッセージ履歴から計画内容を復元する
メッセージ履歴の中には、さらに複数の手がかりが存在しうる。
ExitPlanMode の tool_use input
ユーザーメッセージ内の planContent フィールド
auto-compact によって保持された plan_file_reference
背後にある考え方はシンプルだ。計画は、一度ユーザーに承認されたなら、もはや単なる自然言語の提案ではなく、後続の実行フェーズにおける契約となる。
実行フェーズが知っておく必要があるのは、次のようなことだ。
- 当時承認された目標は何か
- どのステップがスコープに含まれているか
- どのファイルが変更されると見込まれているか
- どのリスクが宣言済みか
- セッションを復元する場合、どの計画から続行すべきか
これが Plan Mode の第二の本質である。計画をチャットの内容から、復元可能なランタイムオブジェクトへと格上げしているのだ。
(自分たちで Agent システムを構築するとき、この点を見落としがちだ。計画をメッセージ履歴に入れておけばそれで十分だと思い込んでしまう。ところがコンテキストが切り詰められた途端に計画も一緒に消え、Agent は実行フェーズに入ったあと自由奔放に動き始める。)
復元と fork もこのライフサイクルに組み込まれている。resume 時には履歴ログから slug を探し、元の計画ファイルを読み取ろうとする。リモート環境でファイルが失われていた場合は、ファイルスナップショットやメッセージ履歴から内容を復元し、ファイルを書き直す。fork session 時には新しい slug を生成し、元の plan ファイルをコピーする。これにより、fork 後の計画変更が元のセッションを上書きするのを防ぐ。
7. 検証フック:実行者以外に必要な「目撃者」
Plan Mode の最後のピースは、計画の検証です。
ローカル資料には registerPlanVerificationHook についての言及があります。これは ExitPlanModeV2Tool の近くで登録され、実行後に実際の結果が計画と合致しているかをチェックするために使われます。
一つ注意点があります。このフックは context clear の後に登録する必要があります。なぜなら、コンテキストのクリアによって既存のフックも削除されてしまうため、登録が早すぎると検証ロジックが消えてしまうからです。
なぜ検証を独立して行うのでしょうか。
実行 Agent が自分自身をチェックすると、「やったつもり」というバイアスが入りやすくなります。自分の実装パスに沿って結果を解釈してしまい、元の計画と項目単位で照合するとは限らないからです。
検証 Agent、あるいは検証フックは、コードレビューに近い役割を果たします。
元の計画を読み込む
-> 実行後の変更を確認する
-> 計画の各項目と照合する
-> 完了項目・乖離項目・未達項目をマークする
-> 検証結果をメインプロセスにフィードバックする
これにより、Plan は単なる「実行前の儀式」ではなく、実行後まで貫かれるチェック基準となります。
8. ページネーション要件から見る完全なフロー
引き続き「REST API にページネーションを追加する」を例に取ろう。
Plan Mode がなければ、エージェントはそのままコードを書き始めるかもしれない。
新規 pagination.ts
users ルートの修正
レスポンス構造の変更
テスト実行
一見効率的だが、プロジェクトの慣習を理解しないまま進んでしまうリスクがある。
Plan Mode があると、フローは次のようになる。
ユーザー:REST API にページネーションを追加して
Claude Code:
1. 複数ファイルに及ぶ実装タスクと判断し、Plan Mode に移行
2. 現在の権限モードを prePlanMode に退避
3. plan 権限に切り替え、読み取り専用の探索のみ許可
4. 既存のインターフェース、レスポンス型、バリデーションミドルウェア、ORM の利用方法を検索
5. 必要に応じて Plan サブエージェントにコードベース調査を委譲
6. offset 方式と cursor 方式の2案を比較
7. ExitPlanMode で計画を提出
8. ユーザーが計画を承認
9. 元の権限モードを復元
10. 計画に沿ってコードを修正
11. テストを実行
12. 実際の変更が計画に合致しているか検証
本当に価値があるのは「手順7で計画を書いた」ことではない。その前後を支えるすべての仕組みが閉ループを形成している点にある。
読み取り専用の調査が、誤った変更を防ぐ
権限の切り替えが、境界を保証する
ユーザー承認が、認識の一致を保証する
計画の永続化が、再開可能性を保証する
検証フックが、逸脱を防ぐ
これこそが Plan Mode の本質的な意味である。
9. Plan Mode と Agent 協調・Sandbox の関係
この章は、本シリーズにおいて Agent 協調の後、Sandbox の前に読むことを想定している。
Agent 協調との関係:Plan サブエージェントは Claude Code に組み込まれたサブエージェント分業の一部である。汎用の実行者ではなく、計画フェーズに特化したリサーチロールを担う。独立したコンテキストによってメインスレッドの汚染を減らし、読み取り専用ツールによって副作用を制限する。
Sandbox との関係:Plan Mode は「いつ行動を許すか」を解決し、Sandbox は「行動を許したときにどこまで触れるか」を解決する。前者はフェーズ境界であり、後者は環境境界である。
おおまかには次のように整理できる。
Plan Mode :実行前に、まず行動を制限する
Permission:ツール呼び出し時に、ask / allow / deny を判断する
Sandbox :コマンドとファイルアクセス時に、影響範囲を制限する
Subagent :複雑なタスクにおいて、コンテキストと責務を隔離する
Hook :ライフサイクルの節目に、追加のチェックと自動化を差し込む
これらの仕組みが合わさることで、Claude Code は単なる「ツールを呼べるモデル」ではなく、真の Agent Harness として機能する。
10. Plan を自作するなら、最小バージョンはどうあるべきか
Claude Code をそのまま模倣するのではなく、設計だけを抽象化するとしたら、最低限使える Plan システムに必要な要素は五つある。
第一に、明示的なモード。
mode = normal | plan
プロンプトだけに頼ってはいけない。ランタイムの状態として、現在 Plan フェーズにいるかどうかを必ず把握できるようにする。
第二に、読み取り専用のツールセット。
plan mode:
allow: Read, Search, List, SafeInspect
deny: Write, Edit, Delete, MutatingShell
第三に、終了時の承認ポイント。
ExitPlan(plan)
-> プランをユーザーに表示
-> 承認 / 却下 / 修正
第四に、プランの永続化。
plan.md
message.tool_use.input.plan
resume snapshot
少なくとも、セッションを再開した後でもユーザーが承認したプランを参照できるようにしなければならない。
第五に、実行後の検証。
approved_plan + actual_diff + test_result
-> 検証レポート
検証がなければ、Plan は「事前に書いた願望リスト」に堕してしまいがちだ。
11. まとめ
Claude Code の Plan 機能は、単なる「計画の生成」ではない。むしろ、エージェントの振る舞いを段階的に統治するコントロールプレーンに近い。
一言で言えば:
Plan Mode は「まず理解し、それから行動する」を、モデルの自発的判断からランタイムの制度的な仕組みへと格上げしたものである。
その核となる設計は、次の五語に圧縮できる:
読み取り専用の探索
権限の切り替え
サブエージェントによる調査
ユーザー承認
復元可能な検証
Claude Code を一つの Agent Harness として捉えると、Plan Mode の意義ははっきりする。それは計画を見栄えよくするためのものではなく、複雑なタスクを着手段階で事前に整合させ、実行中に追跡可能にし、失敗しても復旧できるようにするためのものだ。
これこそが、現代の Coding Agent と単なるチャットボットを分かつ境界線の一つでもある。